Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GasFeeController #494

Merged
merged 46 commits into from
Jul 7, 2021
Merged

Add GasFeeController #494

merged 46 commits into from
Jul 7, 2021

Conversation

danjm
Copy link
Contributor

@danjm danjm commented Jun 17, 2021

This controller handles all logic for fetching gas fee estimates. Currently supports the following estimate types:

FeeMarketEstimateType - EIP-1559 style maxFeePerGas/maxPriorityFeePerGas estimates along with timing data for displaying to users.
LegacyEstimateType - Fast/average/slow type gasPrice estimates, retrieved from the MetaSwaps API
EthGasPriceEstimateType - a single gasPrice estimate fetched from eth_gasPrice.
NoEstimateType - This is the state shape when there is no currently active estimate type.

This controller polls for gas fee estimates at a frequency specified by the interval prop passed into the constructor's options. Polling only happens when there are active subscribers to gas fees. Subscribers are issued a polling token, which can be used to unsubscribe from the controller. When all tokens are removed the polling mechanism stops. This allows multiple views and/or components to indicate their requirement for data.

Comment on lines 121 to 134
async getGasFeeEstimatesAndStartPolling(
pollToken: string,
): Promise<GasFeeState | undefined> {
let gasEstimates;
if (this.pollTokens.size > 0) {
gasEstimates = this.state;
} else {
gasEstimates = await this._fetchGasFeeEstimateData();
}

this._startPolling(pollToken);

return gasEstimates;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not expect start polling to return the gasEstimates since those will be updated in the state and we are following a reactive paradigm, the values should always be read from the state as it is the source of truth.

I would expect though that start polling returns the pollToken or a fresh pollToken in case the argument is falsy, consider this code:

// "Mounting / Unmounting / changing pollToken" effect
useEffect(() => {
  const token = getGasFeeEstimatesAndStartPolling(pollToken);
  setPollToken(token);
  return () => {
	if (token) {
      disconnectPoller(token);
    }
  }
}, [setPollToken, pollToken]);

this would work in the case pollToken is set or not:

const [pollToken, setPollToken] = useState(null);
// or 
const [pollToken, setPollToken] = useState(txHash);

In order to do so:

  • start polling with an existing poll token is a no op
  • start polling with a falsy poll token returns a new poll token

I'd encourage the use of this pattern because we minimize UI <-> Controllers polling issues.

@rickycodes rickycodes force-pushed the draft-gasfee-controller branch from 0029514 to ff358b7 Compare June 30, 2021 02:52
src/util.ts Outdated
export function gweiDecToWEIBN(n: number | string) {
const wholePart = Math.floor(Number(n));
const decimalPartMatch = Number(n)
.toFixed(3)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh. So digits after the third are omitted 🤔

This seems fine, given that WEI isn't really divisible. But this is probably worth documenting at least, so it doesn't come as a surprise (e.g. "rounds down to the nearest WEI".).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, except I realize now that a GWEI is 1,000,000,000 WEI (10^9), and not just 1000 WEI (10^3) as this logic would imply.
🤦

changing to .toFixed(9) should fix that.

@danjm danjm marked this pull request as ready for review July 2, 2021 16:17
@danjm danjm requested a review from a team as a code owner July 2, 2021 16:17
@brad-decker brad-decker force-pushed the draft-gasfee-controller branch from b2a0f11 to 50da3b7 Compare July 2, 2021 16:46
@brad-decker brad-decker changed the title Draft gasfee controller Add GasFeeController Jul 2, 2021
Comment on lines 21 to 25
export async function fetchLegacyGasPriceEstimates(): Promise<{
low: string;
medium: string;
high: string;
}> {
Copy link
Member

@wachunei wachunei Jul 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a reason to not use the LegacyGasPriceEstimate interface?

import { LegacyGasPriceEstimate } from './GasFeeController';

//...

export async function fetchLegacyGasPriceEstimates(): Promise<LegacyGasPriceEstimate> {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, Resolved 53250a9

wachunei
wachunei previously approved these changes Jul 5, 2021
if (error) {
reject(error);
} else {
const isEIP1559Compatible = typeof block.baseFee !== undefined;
Copy link
Member

@andrepimenta andrepimenta Jul 5, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be a string 'undefined', so typeof block.baseFee !== 'undefined';

Copy link
Member

@andrepimenta andrepimenta Jul 5, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, from testing on Ropsten, I believe it should be block.baseFeePerGas?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in 2bdcd7d and baseFeePerGas is what we used on extension's version of this code with success.

): Promise<EthGasPriceEstimate> {
const gasPrice = await query(ethQuery, 'gasPrice');
return {
gasPrice: weiHexToGweiDec(gasPrice).toString(),
Copy link
Member

@andrepimenta andrepimenta Jul 5, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be failing: Invalid character in 0x5f48b0f7. Is weiHexToGweiDec expecting other format?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe without the 0x?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aabfa3a good catch, added a stripHexPrefix in the conversion method and added a test

'Content-Type': 'application/json',
},
});
return {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be slow, average, fast?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about it, but I think the synergy afforded of having gasPriceEstimates.low/medium/high on two of the three valid gas estimate types is better, and the mapping of slow/avg/fast can happen on app side.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, thanks Brad!

@brad-decker
Copy link
Contributor

Thanks for the reviews @andrepimenta and @wachunei -- resolved two of the three comments with code changes and responded to the third. Let me know what you think 🙏🏻

@andrepimenta
Copy link
Member

Thanks for the reviews @andrepimenta and @wachunei -- resolved two of the three comments with code changes and responded to the third. Let me know what you think 🙏🏻

Working great now, thanks!!

const { properties = {} } = this.state;

if (!properties.isEIP1559Compatible) {
if (!this.ethQuery || !this.ethQuery.sendAsync) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can use optional chaining here, if our project supports it:

if (!this?.ethQuery?.sendAsync) {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved :)

@brad-decker brad-decker force-pushed the draft-gasfee-controller branch from a8b4bfd to 5d843cd Compare July 6, 2021 20:37
@brad-decker brad-decker force-pushed the draft-gasfee-controller branch from 5d843cd to 8d04cc5 Compare July 7, 2021 15:58
});

describe('weiHexToGweiDec', () => {
it('should convert a whole number to WEI', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The descriptions of these it blocks need to change to reflect that the function is converting to GWEI

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

41cdace done here

@wachunei
Copy link
Member

wachunei commented Jul 7, 2021

Since GasFeeController constructor requires a getChainId function, should we add a getter to the NetworkController like networkController.getChainId() for simplicity?

});
});

describe('weiHexToGweiDec', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be personal preference, but I think these tests can be clearer in intent, and also less prone to error, if the hard coded inputs and hard coded outputs are explicitly provided, as opposed to be calculated by other functions.

This is how I would write these tests, feel free to copy and paste:

describe('weiHexToGweiDec', () => {
  it('should convert a whole number to WEI', () => {
    const testData = [
      {
        input: '3b9aca00',
        expectedResult: 1,
      },
      {
        input: '1ca35f0e00',
        expectedResult: 123,
      },
      {
        input: '178411b200',
        expectedResult: 101,
      },
      {
        input: '11f5021b400',
        expectedResult: 1234,
      },
    ];
    testData.forEach(({ input, expectedResult }) => {
      expect(
        util.weiHexToGweiDec(input),
      ).toBe(expectedResult);
    });
  });

  it('should convert a number with a decimal part to WEI', () => {
    const testData = [
      {
        input: '4190ab00',
        expectedResult: 1.1,
      },
      {
        input: '1ca3f7a480',
        expectedResult: 123.01,
      },
      {
        input: '178420f440',
        expectedResult: 101.001,
      },
      {
        input: '11f71ed6fc0',
        expectedResult: 1234.567,
      },
    ];

    testData.forEach(({ input, expectedResult }) => {
      expect(
        util.weiHexToGweiDec(input),
      ).toBe(expectedResult);
    });
  });

  it('should convert a number < 1 to WEI', () => {
    const testData = [
      {
        input: '5f5e100',
        expectedResult: 0.1,
      },
      {
        input: '989680',
        expectedResult: 0.01,
      },
      {
        input: 'f4240',
        expectedResult: 0.001,
      },
      {
        input: '21cbbbc0',
        expectedResult: 0.567,
      },
    ];

    testData.forEach(({ input, expectedResult }) => {
      expect(
        util.weiHexToGweiDec(input),
      ).toBe(expectedResult);
    });
  });

  it('should work with 0x prefixed values', () => {
    expect(util.weiHexToGweiDec('0x5f48b0f7')).toBe('1.598599415');
  });
});

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was not going to comment about this, but jest has a test.each function which is my personal preference, either as string literal table or array table.

Example:

test.each`
  a    | b    | expected
  ${1} | ${1} | ${2}
  ${1} | ${2} | ${3}
  ${2} | ${1} | ${3}
`('returns $expected when $a is added $b', ({a, b, expected}) => {
  expect(a + b).toBe(expected);
});

Example from mobile:
https://github.com/MetaMask/metamask-mobile/blob/978b73d88a5208a7d5d74415af980a2f3a59403d/app/components/Base/Keypad/createKeypadRule.test.js

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copy and pasted, just had to update the expectedResult to strings

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brad-decker brad-decker merged commit e594293 into main Jul 7, 2021
@brad-decker brad-decker deleted the draft-gasfee-controller branch July 7, 2021 18:43
This was referenced Jul 7, 2021
MajorLift pushed a commit that referenced this pull request Oct 11, 2023
Co-authored-by: ricky <ricky.miller@gmail.com>
Co-authored-by: Niranjana Binoy <43930900+NiranjanaBinoy@users.noreply.github.com>
Co-authored-by: Brad Decker <git@braddecker.dev>
MajorLift pushed a commit that referenced this pull request Oct 11, 2023
Co-authored-by: ricky <ricky.miller@gmail.com>
Co-authored-by: Niranjana Binoy <43930900+NiranjanaBinoy@users.noreply.github.com>
Co-authored-by: Brad Decker <git@braddecker.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants